/*******************************************************************************
 * Copyright (c) 2014 Steffen Mikkelsen
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     Steffen Mikkelsen - initial API and implementation
 ******************************************************************************/
package com.subsecret.secureconnection.securestream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.subsecret.secureconnection.securestream.layers.StreamLayers;
import com.subsecret.secureconnection.securestream.stream.monitor.MonitorableInputStream;
import com.subsecret.secureconnection.securestream.stream.monitor.MonitorableOutputStream;
import com.subsecret.secureconnection.streamtools.StreamConstants;
import com.subsecret.secureconnection.streamtools.StreamTools;
import com.subsecret.tuples.Tuple.Tuple2;

public class SecureStream {
    
    public static SecureStream createSecureStreamServer(InputStream in, OutputStream out, boolean monitorConnection)
            throws IOException {
        
        SecureStream secureStream = new SecureStream();
        
        if (monitorConnection) {
            MonitorableInputStream monIn = new MonitorableInputStream(in);
            in = monIn;
            MonitorableOutputStream monOut = new MonitorableOutputStream(out);
            out = monOut;
            secureStream.addMonitor("Raw", monIn, monOut);
        }
        
        // Setup each layer
        while (true) {
            int id = StreamTools.receiveInteger(in);
            if (id == StreamConstants.LAYER_NEGOTIATION_FINISHED) {
                break;
            }
            SecureStreamLayer layer = StreamLayers.getLayer(id);
            secureStream.addLayer(layer);
            Tuple2<InputStream, OutputStream> createLayer = layer.createLayerServer(in, out);
            in = createLayer.getFirst();
            out = createLayer.getSecond();
            if (monitorConnection) {
                MonitorableInputStream monIn = new MonitorableInputStream(in);
                in = monIn;
                MonitorableOutputStream monOut = new MonitorableOutputStream(out);
                out = monOut;
                secureStream.addMonitor(layer.getLayerDescription(), monIn, monOut);
            }
        }
        
        // we are done. Finish setup of SecureStream and return it
        secureStream.setStreams(in, out);
        for (SecureStreamLayerMonitor mon : secureStream.getMonitors()) {
            mon.reset();
        }
        return secureStream;
        
    }
    
    public static SecureStream createSecureStreamClient(InputStream in, OutputStream out, boolean monitorConnection,
            SecureStreamLayer... allLayers) throws IOException {
        
        ArrayList<SecureStreamLayer> layers = new ArrayList<>();
        for (SecureStreamLayer layer : allLayers) {
            layers.add(layer);
        }
        Collections.reverse(layers);
        
        SecureStream secureStream = new SecureStream();
        
        // Setup each layer
        for (SecureStreamLayer layer : layers) {
            StreamTools.sendInteger(out, layer.getLayerID());
            if (monitorConnection) {
                MonitorableInputStream monIn = new MonitorableInputStream(in);
                in = monIn;
                MonitorableOutputStream monOut = new MonitorableOutputStream(out);
                out = monOut;
                secureStream.addMonitor(layer.getLayerDescription(), monIn, monOut);
            }
            Tuple2<InputStream, OutputStream> createLayer = layer.createLayerClient(in, out);
            secureStream.addLayer(layer);
            in = createLayer.getFirst();
            out = createLayer.getSecond();
        }
        
        if (monitorConnection) {
            MonitorableInputStream monIn = new MonitorableInputStream(in);
            in = monIn;
            MonitorableOutputStream monOut = new MonitorableOutputStream(out);
            out = monOut;
            secureStream.addMonitor("Raw", monIn, monOut);
        }
        // Tell server that we finished layer negotiation
        StreamTools.sendInteger(out, StreamConstants.LAYER_NEGOTIATION_FINISHED);
        
        // we are done. Finish setup of SecureStream and return it
        secureStream.setStreams(in, out);
        for (SecureStreamLayerMonitor mon : secureStream.getMonitors()) {
            mon.reset();
        }
        return secureStream;
        
    }
    
    private ArrayList<SecureStreamLayerMonitor> layerMonitor = new ArrayList<>();
    private ArrayList<SecureStreamLayer> layers = new ArrayList<>();
    
    private InputStream in;
    private OutputStream out;
    
    public void addLayer(SecureStreamLayer layer) {
        layers.add(layer);
    }
    
    public void addMonitor(String description, MonitorableInputStream in, MonitorableOutputStream out) {
        layerMonitor.add(0, new SecureStreamLayerMonitor(description, in, out));
    }
    
    public InputStream getInputStream() {
        return in;
    }
    
    public OutputStream getOutputStream() {
        return out;
    }
    
    public List<SecureStreamLayerMonitor> getMonitors() {
        return Collections.unmodifiableList(layerMonitor);
    }
    
    public List<SecureStreamLayer> getLayers() {
        return Collections.unmodifiableList(layers);
    }
    
    public static class SecureStreamLayerMonitor {
        private String description;
        private MonitorableInputStream in;
        private MonitorableOutputStream out;
        
        public SecureStreamLayerMonitor(String description, MonitorableInputStream in, MonitorableOutputStream out) {
            this.description = description;
            this.in = in;
            this.out = out;
        }
        
        public String getDescription() {
            return description;
        }
        
        public long getBytesReceived() {
            return in.getBytesTransferred();
        }
        
        public long getBytesSent() {
            return out.getBytesTransferred();
        }
        
        public void reset() {
            in.resetBytesTransferred();
            out.resetBytesTransferred();
        }
        
    }
    
    public void setStreams(InputStream in, OutputStream out) {
        this.in = in;
        this.out = out;
        
    }
}
